<?php

/**
 * OpenIdClient. Authentificate users with OpenID.
 *
 * @author vbolshov
 */

namespace km\openid;

use km\net\HttpRequest;
use km\net\NetworkException;
use \DomDocument;
use \DomXPath;

class OpenIdClient {
	/**
	 * @var string
	 */
	private $base_url, $return_url, $trust_url;
	/**
	 * @var string
	 */
	private $proxy;
	
	/**
	 * @param string $proxy <any-schema>://[username[:password]@]host:port
	 * @return void
	 */
	function setProxy($proxy)
	{
		$this->proxy = $proxy;
	}
	
	/**
	 * @param string $url
	 * @throws OpenIdException
	 */
	function setBaseUrl($url)
	{
		$this->base_url = $this->checkUrl($url);
	}
	/**
	 * @return string
	 * @throws OpenIdException
	 */
	function getTrustUrl()
	{
		if ($this->trust_url)
		{
			return $this->trust_url;
		}
		
		$this->checkBaseUrl();
		
		return $this->base_url;
	}
	/**
	 * @param string $url
	 * @throws OpenIdException
	 */
	function setTrustUrl($url)
	{
		$this->trust_url = $this->checkUrl($url, 'Invalid trust URL');
	}
	/**
	 * @return string
	 * @throws OpenIdException
	 */
	function getReturnUrl()
	{
		if ($this->return_url)
		{
			return $this->return_url;
		}
		
		$this->checkBaseUrl();
		
		return $this->base_url;
	}
	/**
	 * @param string $url
	 * @throws OpenIdException
	 */
	function setReturnUrl($url)
	{
		$this->return_url = $this->checkUrl($url, 'Invalid return URL');
	}
	/**
	 * perform a redirect to OpenID/setup, where the openid server takes control over
	 * user agent.
	 *
	 * @param string $identity
	 * @throws OpenIdException
	 */
	function setup($identity) {
		
		$server = $this->getOpenIdServerUrl($identity);
		
		$params = array(
			'openid.mode' => 'checkid_setup',
			'openid.identity' => $identity,
			'openid.return_to' => $this->getReturnUrl(),
			'openid.trust_root' => $this->getTrustUrl(),
			'openid.assoc_handle' => uniqid('oida-', true),
			'openid.ns.ax' => 'http://openid.net/srv/ax/1.0',
			'openid.sreg.required' => '',
			'openid.sreg.optional' => 'nickname,email,fullname,dob,gender,country,language,timezone',
			'openid.sreg.policy_url' => 'http://localhost/lance-a-lot/credentials-policy.php',
		);
		
		header("Location: $server?" . http_build_query($params, null, '&'));
		exit();
	}
	
	/**
	 * Check user information.
	 *
	 * @return OpenIdUser or bool FALSE
	 * @throws OpenIdException
	 */
	function check()
	{
		if ('id_res' != self::getIncomingParam('mode', FILTER_SANITIZE_STRING))
		{
			throw new OpenIdException("Sorry. OpendID provider did not authentificate you");
		}

		if (! $signed = self::getIncomingParam('signed', FILTER_SANITIZE_STRING))
		{
			throw new OpenIdException("No signed parameters");
		}
		
		if (! $assoc = trim(self::getIncomingParam('assoc_handle', FILTER_SANITIZE_STRING)))
		{
			throw new OpenIdException("openid_assoc_handle not set");
		}
		
		if (! $sig = trim(self::getIncomingParam('sig', FILTER_SANITIZE_STRING)))
		{
			throw new OpenIdException("openid_sig not set");
		}
		
		$identity = self::getIncomingParam('identity', FILTER_VALIDATE_URL);
		
		$data = array(
			'openid.mode' => 'check_authentication',
			'openid.signed' => $signed,
			'openid.assoc_handle' => $assoc,
			'openid.sig' => $sig,
			'openid.return_to' => self::getIncomingParam('return_to', FILTER_VALIDATE_URL),
			'openid.identity' => $identity,
		);
		
		$signed_array = explode(',', $signed);
		foreach ($signed_array as $param) {
			$key = 'openid.' . $param;
			if (! isset($data[$key])) {
				$data[$key] = self::getIncomingParam($param, FILTER_SANITIZE_STRING);
			}
		}
		
		$server = $this->getOpenIdServerUrl($identity);
		
		$req = new HttpRequest($server);
		$req->setProxy($this->proxy);
		$req->setData($data);
		
		list($body) = $req->send();
		
		if ('is_valid:true' == trim($body))
		{
			$user = new OpenIdUser($identity);
			$user->setUsername(filter_input(INPUT_GET, 'openid_sreg_nickname', FILTER_SANITIZE_STRING));
			return $user;
		} else {
			return false;
		}
	}
	
	/**
	 * Take control over user-agent, check input parameters
	 * @param string $identity_key key in $_POST or $_GET (depends on the second argument value) containing identity
	 * @return OpenIdUser or bool FALSE
	 * @throws OpenIdException
	 */
	function takeControl($identity_key)
	{
		// are we gonna to setup?
		$input = ('POST' == $_SERVER['REQUEST_METHOD']) ? INPUT_POST : INPUT_GET;
		if ($identity = filter_input($input, $identity_key, FILTER_VALIDATE_URL))
		{
			$this->setup($identity);
			exit ;// just to make it more clear that the execution halts
		}
		
		if ('GET' != $_SERVER['REQUEST_METHOD'])
		{
			throw new OpenIdException('Illegal request method, should only be GET');
		}
		
		if ('id_res' == filter_input(INPUT_GET, 'openid_mode', FILTER_SANITIZE_STRING))
		{
			return $this->check();
		}
		
		throw new OpenIdException('Could neither setup nor check');
	}
	/**
	 * Check whether this HTTP request is controllable
	 * @return bool
	 */
	static function requestControllable($identity_key)
	{
		
		$input = ('POST' == $_SERVER['REQUEST_METHOD']) ? INPUT_POST : INPUT_GET;
		return filter_input($input, $identity_key, FILTER_VALIDATE_URL) || 
			filter_input(INPUT_GET, 'openid_mode', FILTER_SANITIZE_STRING);
	}
	
	static function getIncomingParam($name, $filter)
	{
		if (($p = @$_GET['openid.' . $name]) || ($p = @$_GET['openid_' . $name])) {
			return filter_var($p, $filter);
		}
	}
	
	/*
	 * -------------- Below go private methods --------------
	 */
	
	private function checkUrl($url, $errorMessage = "Invalid URL")
	{
		if ($url = filter_var($url, FILTER_VALIDATE_URL))
		{
			return $url;
		}
		
		throw new OpenIdException($errorMessage);
	}
	
	private function checkBaseUrl()
	{
		if (empty($this->base_url))
		{
			throw new OpenIdException("Base URL not set");
		}
	}
	/**
	 * @param string $identity
	 * @return string
	 */
	private function getOpenIdServerUrl($identity)
	{
		if (! $identity = filter_var($identity, FILTER_VALIDATE_URL))
		{
			throw new OpenIdException("Error: invalid identity!");
		}
		
		try {
			$req = new HttpRequest($identity);
			$req->setProxy($this->proxy);
			list($body) = $req->send();
		} catch (NetworkException $e) {
			throw new OpenIdException("Could not fetch identity: " . $e->getMessage());
		}
		
		$dom = new DOMDocument('1.0', 'utf-8');
		@$dom->loadHTML($body);// we suppress errors/warnings and this _is_ correct
		$xpath = new DOMXPath($dom);
		foreach ($xpath->query("/html/head/link[@rel='openid.server']") as $node)
		{
			$server = $node->getAttribute('href');
			break ;
		}
		
		if (empty($server))
		{
			throw new OpenIdException("Error fetching identity: OpenID server not found!");
		}
		
		return $server;
	}
}